package com.skaz.dao;

import com.skaz.Constants;
import com.skaz.bean.Page;
import com.skaz.dao.jpa.SimpleJpaDao;
import com.skaz.dao.jpa.support.JdbcSupport;
import com.skaz.dao.jpa.util.Querys;
import com.skaz.util.Reflections;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.Query;
import org.hibernate.Session;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.provider.PersistenceProvider;
import org.springframework.data.jpa.repository.support.JpaEntityInformationSupport;
import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.lang.Nullable;
import org.springframework.orm.jpa.EntityManagerFactoryInfo;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;

import javax.annotation.PostConstruct;
import javax.persistence.EntityManager;
import javax.persistence.LockModeType;
import javax.persistence.PersistenceContext;
import javax.persistence.Table;
import javax.persistence.TypedQuery;
import javax.persistence.metamodel.EntityType;
import javax.persistence.metamodel.Metamodel;
import javax.sql.DataSource;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * @Author: jungle
 * @Date: 2019/1/22 4:52 PM
 */
@Data
@NoRepositoryBean
@NoArgsConstructor
@Transactional(readOnly = true, rollbackFor = {})
public class BaseDao<T, ID> extends SimpleJpaDao<T, ID> {


    protected String entityName;
    protected String entityIdName;
    protected EntityType<T> entityType;
    protected String entityTableName;

    protected Class<T> entityClass;

    protected DataSource dataSource;

    @Autowired
    protected JdbcSupport jdbcSupport;

    @PostConstruct
    public void init() {
        this.entityClass = Reflections.getClassGenricType(getClass());
        this.entityInformation = JpaEntityInformationSupport.getEntityInformation(this.entityClass, entityManager);
        this.provider = PersistenceProvider.fromEntityManager(entityManager);
        this.entityName = this.entityInformation.getEntityName();
        this.entityIdName = this.entityInformation.getIdAttributeNames().iterator().next();
        this.entityType = this.getEntityType();
        this.entityTableName = this.getEntityTableName();
        // Data Info
        this.dataSource = getEntityManagerFactoryInfo().getDataSource();
    }

    @PersistenceContext
    public void setEntityManager(EntityManager entityManager) {
        this.entityManager = entityManager;
    }

    /**
     * 获取实体工厂管理对象
     */
    protected EntityManager getEntityManager() {
        return this.entityManager;
    }

    protected EntityManagerFactoryInfo getEntityManagerFactoryInfo() {
        return (EntityManagerFactoryInfo) this.entityManager.getEntityManagerFactory();
    }

    protected DataSource getDataSource() {
        return this.dataSource;
    }

    protected EntityType<T> getEntityType() {
        Metamodel meta = entityManager.getMetamodel();
        return meta.entity(entityClass);
    }

    /**
     * 获取 Session
     */
    protected Session getSession() {
        return (Session) getEntityManager().getDelegate();
    }

    /**
     * 获取实体名称
     */
    public String getEntityName() {
        return this.entityName;
    }

    /**
     * 获取实体类型
     */
    protected Class<T> getEntityClass() {
        return this.entityClass;
    }

    /**
     * Returns the table name for a given entity type in the {@link EntityManager}.
     * http://stackoverflow.com/a/26420307
     */
    public String getEntityTableName() {
        // Check whether @Table annotation is present on the class.
        Table t = entityClass.getAnnotation(Table.class);
        String tableName = (t == null) ? this.entityType.getName().toUpperCase() : t.name();
        return tableName;
    }

    @Transactional(rollbackFor = {})
    public <S extends T> List<S> save(Iterable<S> entities) {
        return super.saveAll(entities);
    }

    @Transactional(rollbackFor = {})
    public <S extends T> List<S> saveAndFlush(Iterable<S> entities) {
        List<S> result = super.saveAll(entities);
        super.flush();
        return result;
    }

    @Transactional(rollbackFor = {})
    public <S extends T> S create(S entity) {
        super.entityManager.persist(entity);
        return entity;
    }

    @Transactional(rollbackFor = {})
    public <S extends T> S createAndFlush(S entity) {
        S result = this.create(entity);
        super.flush();
        return result;
    }

    @Transactional(rollbackFor = {})
    public <S extends T> List<S> create(Iterable<S> entities) {
        Assert.notNull(entities, ID_MUST_NOT_BE_NULL);
        List<S> result = new ArrayList<>();
        for (S entity : entities) {
            result.add(this.create(entity));
        }
        return result;
    }

    @Transactional(rollbackFor = {})
    public <S extends T> List<S> createAndFlush(Iterable<S> entities) {
        List<S> result = this.create(entities);
        this.flush();
        return result;
    }

    //
    // update
    // ---------------------------------------------------------------------------------------------------


    @Transactional(rollbackFor = {})
    public <S extends T> S update(S entity) {
        return this.entityManager.merge(entity);
    }

    @Transactional(rollbackFor = {})
    public <S extends T> S updateAndFlush(S entity) {
        S result = this.update(entity);
        super.flush();
        return result;
    }

    @Transactional(rollbackFor = {})
    public <S extends T> List<S> update(Iterable<S> entities) {
        Assert.notNull(entities, "The given Iterable of entities not be null!");
        List<S> result = new ArrayList<S>();
        for (S entity : entities) {
            result.add(this.update(entity));
        }
        return result;
    }

    @Transactional(rollbackFor = {})
    public <S extends T> List<S> updateAndFlush(Iterable<S> entities) {
        List<S> result = this.update(entities);
        super.flush();
        return result;
    }


    //
    // remove
    // ---------------------------------------------------------------------------------------------------

    @Transactional(rollbackFor = {})
    public void remove(T entity) {
        this.delete(entity);
    }

    @Transactional(rollbackFor = {})
    public void remove(Iterable<? extends T> entities) {
        this.deleteAll(entities);
    }

    @Transactional(rollbackFor = {})
    public void removeInBatch(Iterable<T> entities) {
        this.deleteInBatch(entities);
    }

    @Transactional(rollbackFor = {})
    public void removeAll() {
        this.deleteAll();
    }

    @Transactional(rollbackFor = {})
    public void removeAllInBatch() {
        this.deleteAllInBatch();
    }

    //
    // get
    // ---------------------------------------------------------------------------------------------------

    public T get(ID id) {
        Assert.notNull(id, ID_MUST_NOT_BE_NULL);
        Class<T> domainType = getDomainClass();
        if (metadata == null) {
            return super.entityManager.find(domainType, id);
        }
        LockModeType type = metadata.getLockModeType();
        Map<String, Object> hints = getQueryHints().withFetchGraphs(entityManager).asMap();
        return type == null ? entityManager.find(domainType, id, hints) : entityManager.find(domainType, id, type, hints);
    }


    //
    // list
    // ---------------------------------------------------------------------------------------------------

    public List<T> list(Iterable<ID> ids) {
        return super.findAllById(ids);
    }

    public List<T> list() {
        return this.list(null,null,null);
    }


    public List<T> list(Map<String, Object> filter) {
        return this.list(filter, null, null);
    }

    public List<T> list(Map<String, Object> filter, Map<String, Object> like) {
        return this.list(filter, like, null);
    }

    public List<T> list(Map<String, Object> filter, Map<String, Object> like, Map<String, Object> sort) {
        return super.findAll(Querys.parseMapQuery(this.entityClass, filter, like), Querys.parseMapSort(sort));
    }

    public List<T> list(Map<String, Object> filter, int size) {
        return this.list(filter, null, null, size);
    }

    public List<T> list(Map<String, Object> filter, Map<String, Object> like, int size) {
        return this.list(filter, like, null, size);
    }

    public List<T> list(Map<String, Object> filter, Map<String, Object> like, Map<String, Object> sort, int size) {
        size = Math.abs(size);
        Pageable pageable = PageRequest.of(0, size, Querys.parseMapSort(sort));
        TypedQuery<T> query = getQuery(Querys.parseMapQuery(this.entityClass, filter, like), pageable);
        query.setFirstResult((int) pageable.getOffset());
        query.setMaxResults(size);
        return query.getResultList();
    }

    //
    // page search
    // ---------------------------------------------------------------------------------------------------


    public Page<T> page(Map<String, Object> filter, int no, int size) {
        return this.page(filter, null, null, no, size);
    }

    public Page<T> page(Map<String, Object> filter, Map<String, Object> like, int no, int size) {
        return this.page(filter, like, null, no, size);
    }

    public Page<T> page(Map<String, Object> filter, Map<String, Object> like, Map<String, Object> sort, int no, int size) {
        org.springframework.data.domain.Page<T> page;
        if (no < 1) {
            no = 1;
        }
        if (size == 0) {
            size = Constants.PAGE_SIZE;
        }
        int pageNo = no - 1;
        int pageSize = Math.abs(size);
        if (size > 0) {
            page = super.findAll(Querys.parseMapQuery(this.entityClass, filter, like), PageRequest.of(pageNo, pageSize, Querys.parseMapSort(sort)));
        } else {
            page = this.findAllWithoutCount(Querys.parseMapQuery(this.entityClass, filter, like), PageRequest.of(pageNo, pageSize, Querys.parseMapSort(sort)));
        }
        return new Page<>(page.getContent(), no, size, page.getTotalElements());
    }


    //
    // count
    // ---------------------------------------------------------------------------------------------------

    public long count(Map<String, Object> filter) {
        return this.count(filter, null);
    }

    public long count(Map<String, Object> filter, Map<String, Object> like) {
        return super.count(Querys.parseMapQuery(this.entityClass, filter, like));
    }

    //
    // exist
    // ---------------------------------------------------------------------------------------------------

    public boolean exists(ID id) {
        return super.existsById(id);
    }


    public boolean exists(Map<String, Object> filter) {
        return this.count(filter) > 0;
    }

    public boolean exists(Map<String, Object> filter, Map<String, Object> like) {
        return this.count(filter, like) > 0;
    }

    //
    // get by sql
    // ---------------------------------------------------------------------------------------------------

    public T getBySql(String sql) {
        return this.jdbcSupport.get(sql, this.entityClass);
    }

    public T getBySql(String sql, Object... params) {
        return this.jdbcSupport.get(sql, this.entityClass, params);
    }

    public T getBySql(String sql, int size) {
        return this.jdbcSupport.get(sql, this.entityClass, size);
    }

    public T getBySql(String sql, Map<String, Object> params) {
        return this.jdbcSupport.get(sql, this.entityClass, params);
    }

    public T getBySql(String sql, Map<String, Object> params, int size) {
        return this.jdbcSupport.get(sql, this.entityClass, params, size);
    }

    //
    // list by sql
    // ---------------------------------------------------------------------------------------------------

    public List<T> listBySql(String sql) {
        return this.jdbcSupport.list(sql, this.entityClass);
    }

    public List<T> listBySql(String sql, int size) {
        return this.jdbcSupport.list(sql, this.entityClass, size);
    }

    public List<T> listBySql(String sql, Map<String, Object> params) {
        return this.jdbcSupport.list(sql, this.entityClass, params);
    }

    public List<T> listBySql(String sql, int size, Map<String, Object> params) {
        return this.jdbcSupport.list(sql, this.entityClass, size, params);
    }

    public List<T> listBySql(String sql, List<Object> params) {
        return this.jdbcSupport.list(sql, this.entityClass, params);
    }

    public List<T> listBySql(String sql, int size, List<Object> params) {
        return this.jdbcSupport.list(sql, this.entityClass, size, params);
    }

    public List<T> listBySql(String sql, Object... params) {
        return this.jdbcSupport.list(sql, this.entityClass, params);
    }

    public List<T> listBySql(String sql, int size, Object... params) {
        return this.jdbcSupport.list(sql, this.entityClass, size, params);
    }

    //
    // get by sql with entityClass
    // ---------------------------------------------------------------------------------------------------

    public <E> E getBySql(String sql, Class<E> entityClass) {
        return this.jdbcSupport.get(sql, entityClass);
    }

    public <E> E getBySql(String sql, Class<E> entityClass, int size) {
        return this.jdbcSupport.get(sql, entityClass, size);
    }

    public <E> E getBySql(String sql, Class<E> entityClass, Map<String, Object> params) {
        return this.jdbcSupport.get(sql, entityClass, params);
    }

    public <E> E getBySql(String sql, Class<E> entityClass, int size, Map<String, Object> params) {
        return this.jdbcSupport.get(sql, entityClass, size, params);
    }

    public <E> E getBySql(String sql, Class<E> entityClass, List<Object> params) {
        return this.jdbcSupport.get(sql, entityClass, params);
    }

    public <E> E getBySql(String sql, Class<E> entityClass, int size, List<Object> params) {
        return this.jdbcSupport.get(sql, entityClass, size, params);
    }

    public <E> E getBySql(String sql, Class<E> entityClass, Object... params) {
        return this.jdbcSupport.get(sql, entityClass, params);
    }

    public <E> E getBySql(String sql, Class<E> entityClass, int size, Object... params) {
        return this.jdbcSupport.get(sql, entityClass, size, params);
    }

    //
    // list by sql with entityClass
    // ---------------------------------------------------------------------------------------------------

    public <E> List<E> listBySql(String sql, Class<E> entityClass) {
        return this.jdbcSupport.list(sql, entityClass);
    }

    public <E> List<E> listBySql(String sql, Class<E> entityClass, int size) {
        return this.jdbcSupport.list(sql, entityClass, size);
    }

    public <E> List<E> listBySql(String sql, Class<E> entityClass, Map<String, Object> params) {
        return this.jdbcSupport.list(sql, entityClass, params);
    }

    public <E> List<E> listBySql(String sql, Class<E> entityClass, int size, Map<String, Object> params) {
        return this.jdbcSupport.list(sql, entityClass, size, params);
    }

    public <E> List<E> listBySql(String sql, Class<E> entityClass, List<Object> params) {
        return this.jdbcSupport.list(sql, entityClass, params);
    }

    public <E> List<E> listBySql(String sql, Class<E> entityClass, int size, List<Object> params) {
        return this.jdbcSupport.list(sql, entityClass, size, params);
    }

    public <E> List<E> listBySql(String sql, Class<E> entityClass, Object... params) {
        return this.jdbcSupport.list(sql, entityClass, params);
    }

    public <E> List<E> listBySql(String sql, Class<E> entityClass, int size, Object... params) {
        return this.jdbcSupport.list(sql, entityClass, size, params);
    }

    //
    // get by sql with RowMapper
    // ---------------------------------------------------------------------------------------------------

    public <E> E getBySql(String sql, RowMapper<E> mapper) {
        return this.jdbcSupport.get(sql, mapper);
    }

    public <E> E getBySql(String sql, RowMapper<E> mapper, int size) {
        return this.jdbcSupport.get(sql, mapper, size);
    }

    public <E> E getBySql(String sql, RowMapper<E> mapper, Map<String, Object> params) {
        return this.jdbcSupport.get(sql, mapper, params);
    }

    public <E> E getBySql(String sql, RowMapper<E> mapper, int size, Map<String, Object> params) {
        return this.jdbcSupport.get(sql, mapper, size, params);
    }

    public <E> E getBySql(String sql, RowMapper<E> mapper, List<Object> params) {
        return this.jdbcSupport.get(sql, mapper, params);
    }

    public <E> E getBySql(String sql, RowMapper<E> mapper, int size, List<Object> params) {
        return this.jdbcSupport.get(sql, mapper, size, params);
    }

    public <E> E getBySql(String sql, RowMapper<E> mapper, Object... params) {
        return this.jdbcSupport.get(sql, mapper, params);
    }

    public <E> E getBySql(String sql, RowMapper<E> mapper, int size, Object... params) {
        return this.jdbcSupport.get(sql, mapper, size, params);
    }

    //
    // list by sql with RowMapper
    // ---------------------------------------------------------------------------------------------------

    public <E> List<E> listBySql(String sql, RowMapper<E> mapper) {
        return this.jdbcSupport.list(sql, mapper);
    }

    public <E> List<E> listBySql(String sql, RowMapper<E> mapper, int size) {
        return this.jdbcSupport.list(sql, mapper, size);
    }

    public <E> List<E> listBySql(String sql, RowMapper<E> mapper, Map<String, Object> params) {
        return this.jdbcSupport.list(sql, mapper, params);
    }

    public <E> List<E> listBySql(String sql, RowMapper<E> mapper, int size, Map<String, Object> params) {
        return this.jdbcSupport.list(sql, mapper, size, params);
    }

    public <E> List<E> listBySql(String sql, RowMapper<E> mapper, List<Object> params) {
        return this.jdbcSupport.list(sql, mapper, params);
    }

    public <E> List<E> listBySql(String sql, RowMapper<E> mapper, int size, List<Object> params) {
        return this.jdbcSupport.list(sql, mapper, size, params);
    }

    public <E> List<E> listBySql(String sql, RowMapper<E> mapper, Object... params) {
        return this.jdbcSupport.list(sql, mapper, params);
    }

    public <E> List<E> listBySql(String sql, RowMapper<E> mapper, int size, Object... params) {
        return this.jdbcSupport.list(sql, mapper, size, params);
    }

    //
    // page by sql
    // ---------------------------------------------------------------------------------------------------

    public Page<T> pageBySql(String sql, int no, int size) {
        return this.jdbcSupport.page(sql, this.entityClass, no, size);
    }

    public Page<T> pageBySql(String sql, int no, int size, Map<String, Object> params) {
        return this.jdbcSupport.page(sql, this.entityClass, no, size, params);
    }

    public Page<T> pageBySql(String sql, int no, int size, Object... params) {
        return this.jdbcSupport.page(sql, this.entityClass, no, size, params);
    }

    //
    // page by sql with entityClass
    // ---------------------------------------------------------------------------------------------------

    public <E> Page<E> pageBySql(String sql, Class<E> entityClass, int no, int size) {
        return this.jdbcSupport.page(sql, entityClass, no, size);
    }

    public <E> Page<E> pageBySql(String sql, Class<E> entityClass, int no, int size, Map<String, Object> params) {
        return this.jdbcSupport.page(sql, entityClass, no, size, params);
    }

    public <E> Page<E> pageBySql(String sql, Class<E> entityClass, int no, int size, Object... params) {
        return this.jdbcSupport.page(sql, entityClass, no, size, params);
    }

    //
    // page by sql with RowMapper
    // ---------------------------------------------------------------------------------------------------

    public <E> Page<E> pageBySql(String sql, RowMapper<E> mapper, int no, int size) {
        return this.jdbcSupport.page(sql, mapper, no, size);
    }

    public <E> Page<E> pageBySql(String sql, RowMapper<E> mapper, int no, int size, Map<String, Object> params) {
        return this.jdbcSupport.page(sql, mapper, no, size, params);
    }

    public <E> Page<E> pageBySql(String sql, RowMapper<E> mapper, int no, int size, Object... params) {
        return this.jdbcSupport.page(sql, mapper, no, size, params);
    }

    //
    // count by sql
    // ---------------------------------------------------------------------------------------------------

    public long countBySql(String sql) {
        return this.jdbcSupport.count(sql);
    }

    public long countBySql(String sql, List<Object> params) {
        return this.jdbcSupport.count(sql, params);
    }

    public long countBySql(String sql, Map<String, Object> params) {
        return this.jdbcSupport.count(sql, params);
    }

    public long countBySql(String sql, Object... params) {
        return this.jdbcSupport.count(sql, params);
    }

    //
    // create sql
    // ---------------------------------------------------------------------------------------------------

    public Query createSqlQuery(String sql, Object params) {
        return createSqlQuery(sql, params, false);
    }

    public Query createSqlQuery(String sql, Object params, boolean cacheable) {
        return Querys.createSqlQuery(getSession(), sql, params, cacheable);
    }

    //
    // execute by sql
    // ---------------------------------------------------------------------------------------------------

    @Transactional(rollbackFor = {})
    public int executeSql(String sql) {
        return this.createSqlQuery(sql, null).executeUpdate();
    }

    @Transactional(rollbackFor = {})
    public int executeSql(String sql, Object... params) {
        return this.createSqlQuery(sql, params).executeUpdate();
    }

    @Transactional(rollbackFor = {})
    public int executeSql(String sql, Map<String, ?> params) {
        return this.createSqlQuery(sql, params).executeUpdate();
    }

    //
    // query for List<?>
    // ---------------------------------------------------------------------------------------------------

    public List<String> listBySqlForStringList(String sql, Object... params) {
        return this.jdbcSupport.queryForStringList(sql, params);
    }

    public List<Long> listBySqlForLongList(String sql, Object... params) {
        return this.jdbcSupport.queryForLongList(sql, params);
    }

    public List<Integer> listBySqlForIntegerList(String sql, Object... params) {
        return this.jdbcSupport.queryForIntegerList(sql, params);
    }

    //
    // state
    // http://blog.csdn.net/sunhuwh/article/details/9183539
    // https://stackoverflow.com/a/3683370
    // ---------------------------------------------------------------------------------------------------

    public void detach(T entity) {
        this.entityManager.detach(entity);
    }

    public void lock(T entity, LockModeType lockMode) {
        this.entityManager.lock(entity, lockMode);
    }

    public void clear() {
        this.entityManager.clear();
    }

    protected org.springframework.data.domain.Page<T> findAllWithoutCount(@Nullable Specification<T> spec, Pageable pageable) {
        TypedQuery<T> query = getQuery(spec, pageable);
        return isUnpaged(pageable) ? new PageImpl<T>(query.getResultList()) : readPageWithoutCount(query, getDomainClass(), pageable, spec);
    }

    protected <S extends T> org.springframework.data.domain.Page<S> readPageWithoutCount(TypedQuery<S> query, final Class<S> domainClass, Pageable pageable, @Nullable Specification<S> spec) {
        int pageNo = pageable.getPageNumber();
        int pageSize = pageable.getPageSize();
        query.setFirstResult((int) pageable.getOffset());
        int expectedSize = pageable.getPageSize() + 1;
        query.setMaxResults(expectedSize);
        List<S> content = query.getResultList();
        int contentSize = content.size();
        if (contentSize == expectedSize) {
            content.remove(content.size() - 1);
        }
        long total = contentSize > 0 ? pageNo * pageSize + contentSize : 0;
        return new PageImpl<>(content, PageRequest.of(pageNo, pageSize), total);
    }

    protected static boolean isUnpaged(Pageable pageable) {
        return pageable.isUnpaged();
    }

}
